import {
  TEqualFunc,
  defaultEaquals,
} from '../utils/tools';

export type TElements = any[]; 
export interface IPushNodeReturn<T> {
  newNode: TTNulish<T>;
  preNode: TTNulish<T>;
}
export interface IRemoveAtReturn<T> {
  nodeRemove: TTNulish<T>;
  preNode: TTNulish<T>;
}

export interface IInsertReturn<T> {
  newNode: TTNulish<T>;
  nextNode: TTNulish<T>;
  preNode: TTNulish<T>;
}

export class LinkedNodeBase {
  public value: any;
  public next: TTNulish<LinkedNodeBase>;
  
  public constructor(element: any) {
    this.value = element;
    this.next = null;
  }
}

export class LinkedNodeDoubly extends LinkedNodeBase {
  public previous: TTNulish<LinkedNodeDoubly>;
  public next: TTNulish<LinkedNodeDoubly>;
  public constructor(element: any) {
    super(element);
    this.previous = null;
    this.next = null;
  }
}

interface ILinkedNode<T> {
  next: TTNulish<T>;
  value: any;
  previous?: TTNulish<T>;
}
type TLinkedNodeFactory<T> = new (element: any) => T;

export class LinkedListBase<T extends ILinkedNode<T>> {
  protected head: TTNulish<T>;
  protected count: number;
  protected equalFunc: TEqualFunc;
  protected NodeTypeFunc: TLinkedNodeFactory<T>;

  public constructor(NodeTypeFunc: TLinkedNodeFactory<T>, elements: TElements = [], equalFunc?: TEqualFunc) {
    this.head = null;
    this.count = 0;
    this.equalFunc = equalFunc || defaultEaquals;
    this.NodeTypeFunc = NodeTypeFunc;
    
    for (let i of elements) {
      void this.insert(i, this.count);
    }
  }

  public getElementAt(index: number) {
    return this.getNodeAt(index)?.value;
  }

  public removeAt(index: number): IRemoveAtReturn<T> | undefined | TTNulish<T> {
    if (index >= 0 && index < this.count) {
      let current: TTNulish<T> = this.head;
      let previous: TTNulish<T> = null;
      
      if (index === 0) {
        this.head = current?.next;
      } else {
        previous = this.getNodeAt(index - 1);
        current = previous?.next;
        previous && (previous.next = current?.next);
      }

      this.count--;
      return {
        nodeRemove: current,
        preNode: previous,
      };
    }
    return undefined;
  }

  public insert(element: any, index: number): IInsertReturn<T> | boolean {
    if (index >=0 && index <= this.count) {
      const newNode = new this.NodeTypeFunc(element);

      let current: TTNulish<T> = null;
      let previous: TTNulish<T> = null;

      if (index === 0) {
        newNode.next = this.head;
        this.head = newNode;
        current = newNode.next;
      } else {
        previous = this.getNodeAt(index - 1);
        current = previous?.next;
        previous && (previous.next = newNode);
        newNode.next = current;
      }
      this.count++;
      return {
        newNode,
        preNode: previous,
        nextNode: current,
      };
    }
    return false;
  }

  public indexOf(element: any) {
    let current = this.head;
    let index = 0;
    while(current) {
      if (this.equalFunc(current.value, element)) {
        return index;
      }
      index++;
      current = current.next;
    }
    return -1;
  }

  public getHead() {
    return this.head?.value;
  }

  public isEmpty() {
    return this.count === 0;
  }

  public size() {
    return this.count;
  }
  
  public toString() {
    let str: string;
    if (this.isEmpty()) {
      str = '';
    } else {
      str = `${JSON.stringify(this.head?.value)}`;
      let current = this.head?.next;
      
      while(current) {
        str = `${str},${JSON.stringify(current?.value)}`;
        current = current.next;
      }
    }

    return str;
  }

  public clear() {
    this.head = null;
    this.count = 0;
  }

  public getNodeAt(index: number): TTNulish<T> {
    if (index >= 0 && index < this.count) {
      let current = this.head;
  
      for (let i = 0; i < index; i++) {
        current = current?.next;
      }
  
      return current;
    }

    return undefined;
  }
}

export class LinKedListNoOrder<T extends ILinkedNode<T>> extends LinkedListBase<T> {
  public constructor(NodeTypeFunc: TLinkedNodeFactory<T>, elements: TElements = [], equalFunc?: TEqualFunc) {
    super(NodeTypeFunc, elements, equalFunc);
  }

  public push(element: any): IPushNodeReturn<T> | void {
    const newNode = new this.NodeTypeFunc(element);
    let current: T | null = null;

    if (!this.head) {
      this.head = newNode;
    } else {
      current = this.head;

      while (current.next) {
        current = current.next;
      }

      current.next = newNode;
    }
    
    this.count++;

    return {
      newNode,
      preNode: current,
    };
  }
}

export class CircularLinkedListBase<T extends ILinkedNode<T>> extends LinKedListNoOrder<T> {
  public constructor(NodeTypeFunc: TLinkedNodeFactory<T>, elements: TElements = [], equalFunc?: TEqualFunc) {
    super(NodeTypeFunc, elements, equalFunc);
  }

  public push(element: any): IPushNodeReturn<T> | void {
    const newNode = new this.NodeTypeFunc(element);
    let current = this.head;
    if (!this.head) {
      this.head = newNode;
      this.head.next = this.head;
      current = this.head;
    } else {
      while(current?.next !== this.head) {
        current = current?.next;
      }
      current.next = newNode;
      newNode.next = this.head;
    }
    this.count++;
    return {
      newNode,
      preNode: current,
    };
  }

  public removeAt(index: number): IRemoveAtReturn<T> | TTNulish<T> {
    const valueReturn = super.removeAt(index);
    if (valueReturn) {
      let { nodeRemove, preNode, } = valueReturn as IRemoveAtReturn<T>;
      
      if (index === 0) {
        const tail: TTNulish<T> = this.getNodeAt(this.count - 1);
        tail && (tail.next = this.head);
        preNode = tail;
      }
      return {
        nodeRemove,
        preNode,
      };
    }

    return valueReturn;
  }

  public insert(element: any, index: number): IInsertReturn<T> | boolean {
    const valueReturn = super.insert(element, index);

    if (valueReturn) {
      let { newNode, preNode, nextNode } = valueReturn as IInsertReturn<T>;
      if (index === 0) {
        const tail: TTNulish<T> = this.getNodeAt(this.count - 1);
        tail && (tail.next = this.head);
        preNode = tail;
      }
      return {
        newNode,
        preNode,
        nextNode,
      };
    }

    return valueReturn;
  }

  public indexOf(element: any) {
    if (this.isEmpty()) {
      return -1;
    } else {
      let current = this.head;
      let index = 0;
      while(current) {
        if (this.equalFunc(current.value, element)) {
          return index;
        }
        index++;
        if (current.next === this.head) {
          current = null;
        } else {
          current = current.next;
        }
      }
      return -1;
    }
  }
  public toString() {
    let str: string;
    if (this.isEmpty()) {
      str = '';
    } else {
      str = `${JSON.stringify(this.head?.value)}`;
      let current = this.head?.next;
      
      while(current !== this.head) {
        str = `${str},${JSON.stringify(current?.value)}`;
        current = current?.next;
      }
    }

    return str;
  }
}
